Linux lock and unlock memory

mlock, mlock2, munlock, mlockall, munlockall - lock and unlock memory

Standard C library (libc, -lc)

#include 

int mlock(const void addr[.size], size_t size);
int mlock2(const void addr[.size], size_t size, unsigned int flags);
int munlock(const void addr[.size], size_t size);

int mlockall(int flags);
int munlockall(void);

flags

- MLOCK_ONFAULT: Lock pages that are currently resident and mark the entire range so that the remaining nonresident pages are locked when they are populated by a page fault. If flags is 0, mlock2() behaves exactly the same as mlock().

mlock(), mlock2(), and mlockall() lock part or all of the calling process's virtual address space into RAM, preventing that memory from being paged to the swap area.

munlock() and munlockall() perform the converse operation, unlocking part or all of the calling process's virtual address space, so that pages in the specified virtual address range can be swapped out again if required by the kernel memory manager.

Memory locking and unlocking are performed in units of whole pages.

Some doc said the addr need to be page-aligned, but I tested on my computer, both addr and size do not need to be page-aligned

fn main() {
    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;

    let mut buf: Vec = Vec::with_capacity(page_size * 2);
    unsafe { buf.set_len(page_size * 2) };

    let addr = buf.as_mut_ptr() as *mut libc::c_void;
    if (addr as usize) % page_size != 0 {
        println!("buf is not page-aligned");
    }
    let len = buf.capacity() * std::mem::size_of::();
    let flags = libc::MLOCK_ONFAULT;
    let res = unsafe { libc::mlock2(addr, len, flags) };

    println!("page_size: {page_size}, addr: {addr:?}, len: {len}, flags: {flags:?}, res: {res}");

    if res != 0 {
        println!("{:?}", std::io::Error::last_os_error());
    }
}

Execution Result:

❯ cargo run
   Compiling ...
   ...
buf is not page-aligned
page_size: 4096, addr: 0x58ae7c7ddb10, len: 65536, flags: 1, res: 0

However, result of locking memory should be page-aligned. We can use the following code to test it out.

use std::fs;
use std::io::{self};

fn main() {
    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;

    let mut buf: Vec = Vec::with_capacity(page_size + 3);

    let addr = buf.as_mut_ptr() as *mut libc::c_void;
    let offset = (addr as usize) % page_size;
    if offset != 0 {
        println!("buf is not page-aligned");
    }
    let len = buf.capacity() * std::mem::size_of::();
    let flags = libc::MLOCK_ONFAULT;
    let res = unsafe { libc::mlock2(addr, len, flags) };

    if res != 0 {
        println!("{:?}", std::io::Error::last_os_error());
    }

    let buf_start = addr;
    let buf_end = unsafe { addr.add(len) };
    println!("Buf memory:");
    println!("  [{buf_start:?}-{buf_end:?}]");

    let align_start = unsafe { addr.sub(offset) };
    let align_end = unsafe { align_start.add(((len + page_size - 1) / page_size) * page_size) };

    println!("Locked memory:");
    println!("- expected:");
    println!("  [{align_start:?}-{align_end:?}]");

    let pid = std::process::id();
    let locked = list_locked_regions(pid).unwrap();

    println!("- real:");
    for (start, end) in locked {
        println!("  [{:#x}-{:#x}]", start, end);
    }
}

fn list_locked_regions(pid: u32) -> io::Result> {
    let maps_path = format!("/proc/{}/maps", pid);
    let smaps_path = format!("/proc/{}/smaps", pid);

    let maps_data = fs::read_to_string(maps_path)?;
    let mut regions = Vec::new();

    for line in maps_data.lines() {
        if let Some((range, _)) = line.split_once(' ') {
            if let Some((start, end)) = range.split_once('-') {
                let start_addr = u64::from_str_radix(start, 16)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let end_addr = u64::from_str_radix(end, 16)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

                regions.push((start_addr, end_addr));
            }
        }
    }

    let smaps_data = fs::read_to_string(smaps_path)?;
    let mut locked_regions = Vec::new();
    let mut current_region = None;

    for line in smaps_data.lines() {
        if let Some((range, _)) = line.split_once(' ') {
            if let Some((start, end)) = range.split_once('-') {
                let start_addr = u64::from_str_radix(start, 16).ok();
                let end_addr = u64::from_str_radix(end, 16).ok();
                current_region = start_addr.and_then(|s| end_addr.map(|e| (s, e)));
            }
        }

        if line.starts_with("Locked:") {
            if let Some(locked) = line.split_whitespace().nth(1) {
                if locked != "0" && locked != "0\n" {
                    if let Some((start, end)) = current_region {
                        locked_regions.push((start, end));
                    }
                }
            }
        }
    }

    Ok(locked_regions)
}

Execution Result

❯ cargo run
   Compiling  ...
     ...
     Running  ...
     
buf is not page-aligned
Buf memory:
  [0x5ce1c3118b10-0x5ce1c3120b28]
Locked memory:
- expected:
  [0x5ce1c3118000-0x5ce1c3121000]
- real:
  [0x5ce1c3118000-0x5ce1c3121000]

Ref

https://man7.org/linux/man-pages/man2/mlock.2.html